﻿//-----------------------------------------------------------------------
// <copyright file="CssPreProcessorHttpHandler.cs" company="Erik Lieben">
//     Copyright Erik Lieben. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace ErikLieben.CssPreprocessor
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Linq;
    using System.Web;

    /// <summary>
    /// The css pre processor http handler
    /// </summary>
    public class CssPreProcessorHttpHandler : IHttpHandler
    {
        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <value></value>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns>
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server 
        /// objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            // Method contracts
            if (context == null)
            {
                throw new ArgumentNullException("Context cannot be null");
            }

            Contract.Requires(context.Request != null);
            Contract.Requires(context.Request.Url != null);
            Contract.Requires(string.IsNullOrWhiteSpace(context.Request.Url.AbsolutePath));

            string fileNameToProcess = context.Request.Url.AbsolutePath;

            // Check if a .css file was requested
            if (!fileNameToProcess.EndsWith("css", StringComparison.OrdinalIgnoreCase))
            {
                throw new System.Web.HttpException("Invalid CSS file extension");
            }

            // retrieve and cleanup the code for processing
            string cssCode = RetrieveLocalCss(fileNameToProcess);
            cssCode = Cleanup.CleanUpCss(cssCode);

            // process variables
            Dictionary<string, string> variables = VariablesProcessor.Process(cssCode, new Dictionary<string, string>());
            cssCode = Cleanup.RemoveVariableTags(cssCode);

            // Build the css container
            List<CssDefinition> cssContainer = CssBuilder.BuildCssDefinition(cssCode);

            // Process variables
            ProcessVariables(variables, cssContainer);
            
            // Process the extensions
            cssContainer = CssBuilder.ProcessCssExtensions(cssContainer);

            // Cleanup the empty tags
            cssContainer.RemoveAll(def => def.Properties.Count == 0);

            // Cleanup extension tags without params
            cssContainer.RemoveAll(def => def.Selectors.Any(sel => sel.Parameters.Count > 0));

            // Cleanup known prefixes
            

            // Perform math on values
            PerformMath(cssContainer);

            // Generate new css code
            cssCode = CssBuilder.BuildCssCode(cssContainer);

            // return the csscode
            if (!string.IsNullOrEmpty(cssCode))
            {
                SetHeaders(cssCode.GetHashCode(), context);
                context.Response.Write(cssCode);
            }
            else
            {
                context.Response.Status = "404 Bad Request";
            }
        }

        // TODO: Fix for full math stuff, just a test for now.
        private void PerformMath(List<CssDefinition> cssContainer)
        {
            foreach (var c in cssContainer)
            {
                foreach (var p in c.Properties)
                {
                    if (p.Value.Contains(" + "))
                    {
                        string[] b = p.Value.Split(new[] { '+' });
                        b[0] = b[0].Replace("#", string.Empty).Trim();
                        b[1] = b[1].Replace("#", string.Empty).Trim();

                        int a1 = int.Parse(b[0].Substring(0, 2), 
                            System.Globalization.NumberStyles.HexNumber);
                        int a2 = int.Parse(b[0].Substring(2, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int a3 = int.Parse(b[0].Substring(4, 2),
                            System.Globalization.NumberStyles.HexNumber);

                        int b1 = int.Parse(b[1].Substring(0, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int b2 = int.Parse(b[1].Substring(2, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int b3 = int.Parse(b[1].Substring(4, 2),
                            System.Globalization.NumberStyles.HexNumber);

                        int newVal1 = (a1 + b1);
                        int newVal2 = (a2 + b2);
                        int newVal3 = (a3 + b3);
                        if (newVal1 > 255) newVal1 = 255;
                        if (newVal2 > 255) newVal2 = 255;
                        if (newVal3 > 255) newVal3 = 255;

                        string hex1 = newVal1.ToString("X");
                        string hex2 = newVal2.ToString("X");
                        string hex3 = newVal3.ToString("X");
                        if (hex1.Length == 1) hex1 += "0";
                        if (hex2.Length == 1) hex2 += "0";
                        if (hex3.Length == 1) hex3 += "0";

                        p.Value = ("#" + hex1 + hex2 + hex3).ToLowerInvariant();
                    }
                    else if (p.Value.Contains(" - "))
                    {
                        
                        string[] b = p.Value.Split(new[] { '-' });
                        b[0] = b[0].Replace("#", string.Empty).Trim();
                        b[1] = b[1].Replace("#", string.Empty).Trim();

                        int a1 = int.Parse(b[0].Substring(0, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int a2 = int.Parse(b[0].Substring(2, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int a3 = int.Parse(b[0].Substring(4, 2),
                            System.Globalization.NumberStyles.HexNumber);

                        int b1 = int.Parse(b[1].Substring(0, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int b2 = int.Parse(b[1].Substring(2, 2),
                            System.Globalization.NumberStyles.HexNumber);
                        int b3 = int.Parse(b[1].Substring(4, 2),
                            System.Globalization.NumberStyles.HexNumber);

                        int newVal1 = (a1 - b1);
                        int newVal2 = (a2 - b2);
                        int newVal3 = (a3 - b3);
                        if (newVal1 > 255) newVal1 = 255;
                        if (newVal2 > 255) newVal2 = 255;
                        if (newVal3 > 255) newVal3 = 255;
                        if (newVal1 < 0) newVal1 = 0;
                        if (newVal2 < 0) newVal2 = 0;
                        if (newVal3 < 0) newVal3 = 0;

                        string hex1 = newVal1.ToString("X");
                        string hex2 = newVal2.ToString("X");
                        string hex3 = newVal3.ToString("X");
                        if (hex1.Length == 1) hex1 += "0";
                        if (hex2.Length == 1) hex2 += "0";
                        if (hex3.Length == 1) hex3 += "0";

                        p.Value = ("#" + hex1 + hex2 + hex3).ToLowerInvariant();
                    }
                }
            }
        }

        /// <summary>
        /// Processes the variables.
        /// </summary>
        /// <param name="variables">The variables to process.</param>
        /// <param name="cssContainer">The CSS container to process the variables on.</param>
        private static void ProcessVariables(Dictionary<string, string> variables, List<CssDefinition> cssContainer)
        {
            foreach (var kvp in variables)
            {
                cssContainer.ForEach(cssdef =>
                {
                    foreach (var prop in cssdef.Properties)
                    {
                        if (prop.Value.ToLowerInvariant().Contains(kvp.Key.ToLowerInvariant()))
                        {
                            prop.Value = prop.Value.Replace(kvp.Key, kvp.Value);
                        }
                    }
                });
            }
        }

        /// <summary>
        /// Retrieves the local CSS from the disk
        /// </summary>
        /// <param name="file">The file to load the css code from.</param>
        /// <returns>the retrieved css as string</returns>
        private static string RetrieveLocalCss(string file)
        {
            string path = HttpContext.Current.Server.MapPath(file);
            try
            {
                return File.ReadAllText(path);
            }
            catch
            { 
                // TODO: Figure out what a nice way of error handling would be
                return string.Empty; 
            }
        }

        /// <summary>
        /// This will make the browser and server keep the output
        /// in its cache to improve performance.
        /// </summary>
        /// <param name="hash">The hash of the css string.</param>
        /// <param name="context">The currently http context.</param>
        private static void SetHeaders(int hash, HttpContext context)
        {
            context.Response.ContentType = "text/css";
            context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;

            context.Response.Cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
            context.Response.Cache.SetMaxAge(new TimeSpan(5, 0, 0, 0));
            context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);

            string etag = "\"" + hash.ToString() + "\"";
            string incomingEtag = context.Request.Headers["If-None-Match"];

            context.Response.Cache.SetETag(etag);

            if (String.Compare(incomingEtag, etag) == 0)
            {
                context.Response.Clear();
                context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
                context.Response.SuppressContent = true;
            }
        }
    }
}

